聊聊 Vue3 中对 Set/Map 的处理 您所在的位置:网站首页 vue map删除 聊聊 Vue3 中对 Set/Map 的处理

聊聊 Vue3 中对 Set/Map 的处理

2023-12-30 18:55| 来源: 网络整理| 查看: 265

前言

最近看 Vue3 源码,看到 Vue 对 Map/Set 做了很多特殊处理,把他们身上的所有方法都又实现了一遍,引起了一些思考与尝试,写篇文章分享出来

先说一个新奇的发现:

Proxy 是无法直接拦截 Set/Map 的!因为 Set/Map 的方法必须得在它们自己身上调用

看到这句话你不禁会想 Vue 是如何代理他们的,继续看下去吧

方法的三种调用形式

本节探讨 Set/Map 它们实例方法的三种调用形式

这里不包括在它们示例身上调用,自己调用当然能正常运行

在 Proxy 对象上调用

用 Proxy 代理一个集合,不做任何拦截,然后调用 add 方法,寄!

const p = new Proxy(new Set(), {}) p.add(1) // TypeError: Method Set.prototype.add called on incompatible receiver #

虽然没法运行方法,但好消息是 Proxy 能拦截到方法的读取,这是下文能够使用 Proxy 包装 Set/Map 的基础

const p = new Proxy(new Set(), { get(target, key) { console.log('get:', key) return Reflect.get(target, key) }, }) p.add(1) // get: add // TypeError: Method Set.prototype.add called on incompatible receiver # 在继承集合的对象上调用

创建一个对象继承一个集合,尝试调用 add 方法,也是寄!

const obj = Object.create(new Set()) obj.add(1) // TypeError: Method Set.prototype.add called on incompatible receiver # const obj = {} Object.setPrototypeOf(obj, new Set()) obj.add(1) // TypeError: Method Set.prototype.add called on incompatible receiver # 在子类身上调用

难道就没有办法在其他对象身上调用 Map/Set 的方法了吗?

还是有的,就是它们的子类示例

class mySet extends Set { constructor() { super() } add(value) { super.add(value) console.log('终于成功运行了') return this } } let set = new mySet() set.add(1) // 终于成功运行了 console.log(set) // mySet(1) [Set] { 1 } 结果

经过上述实验,我们知道了想要拦截 Set/Map,最简单的方式是为它们设置子类

但是,Vue 并没有采用这种方法,原因也很简单,class 关键期 IE13(Edge13)才出,而 Vue 想兼容到 IE12。

而且这一特性是 babel 解决不了的,就是垫不起来

所以呢,Vue3 还是选择用 Proxy 重写方法来解决,接下来让我们看看具体是怎么实现的

用 Proxy 包装 Set 实现思路

既然 Set/Map 的方法只能在原对象上调用,那我们就封装一套方法,先获取原对象,再在它们身上调用方法就好了

就像下面这样

const p = new Proxy(new Set(), { get(target, key) { if (key === 'add') return add // 返回自己实现的方法 return Reflect.get(target, key) }, }) function add(value) { const rawTarget = toRaw(this) // 获取代理的原对象 rawTarget.add(value) // 原对象再调用 add 方法 return this // add方法会返回集合本身 }

toRaw 是 Vue 实现的一个 api,用来获取代理对象的原对象

实现 toRaw

toRaw 实现起来也很简单,毕竟代理对象的拦截器是咱们自己写的,只要在其中定义一个特殊的属性,让拦截器返回原对象就行

const p = new Proxy(new Set(), { get(target, key) { if (key === '__v_raw') return target // 访问特殊属性,返回原对象 if (key === 'add') return add // 返回自己实现的方法 return Reflect.get(target, key) }, }) // 获取原对象的方法 function toRaw(p) { return p['__v_raw'] }

Vue 中考虑到多层代理嵌套的问题,所以源码中 toRaw 的实现是递归调用的,直至对象没有 '__v_raw' 属性

toRaw 实现后,add 函数就已经能够正常运行了

p.add(1) p.add(2) console.log(p) // Proxy { 1, 2 } 浏览器控制台输出 // Set(2) { 1, 2 } node控制台输出

Vue 就是使用这一方式,实现了对 Set/Map 的代理

Vue 中的具体实现

在这里展示一部分 Vue3 的源码,主要是 reactive 方法中对 Set/Map 做的特殊处理

展开或修改了一些函数的调用,但逻辑不变

function reactive(target) { let proxy // 代理对象 const type = Object.prototype.toString.call(target) // 获取类签名 // 对 Set 和 Map 特殊处理 if (type === '[object Map]' || type === '[object Set]') { // 使用 collectionHandlers proxy = new Proxy(target, collectionHandlers) // 将代理对象设置到全局Map中,我们不具体实现 proxyMap.set(target, proxy) } return proxy } const collectionHandlers = { get(target, key) { if (key === '__v_raw') return target // 访问特殊属性,返回原对象 // 如果是Set/Map的原生方法,返回自己封装的方法 // 否则返回对象身上的属性 return Reflect.get(instrumentations.hasOwnProperty(key) ? instrumentations : target, key) }, } // 重写了Set/Map的所有原生方法和属性 const instrumentations = { get, set, add, has, delete: deleteEntry, clear, forEach, get size() { return size(this) }, }

instrumentations 中方法的重写代码就不展示了,简单总结一下,感兴趣的自行去查看源码

所有方法都是通过 toRaw(this) 获取了原对象,在其身上尝试调用方法。并且对所有传入的参数也解了代理 rawKey = toRaw(key) ,以确保存入 Set/Map 中的都是原对象。

在执行 get forEach 方法获取数据时,会再次使用 reactive 包装

在 get has forEach size 函数中跟踪依赖(track)

在 set delete clear add 函数中触发扳机(trigger)

Vue 还重写了迭代器属性/方法(['keys', 'values', 'entries', Symbol.iterator]),以确保迭代器产生的值都被 reactive 包装记录

最后,Vue 对 Set/Map 代理后的结果是:真正存入的对象都是解代理后的原对象,但想从其中取出对象都会自动代理后再返回

其实重写的很多方法都做了两手准备,对已代理和未代理的参数都尝试执行了一遍,这是为了避免有小可爱先用 Set 存了代理对象,再将其传给 Vue

结语

如果喜欢或有所帮助的话,希望能点赞关注,鼓励一下作者。

如果文章有不正确或存疑的地方,欢迎评论指出。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有